Skip to content

ENG-3044: Add search to Admin UI sidebar navigation#7723

Merged
galvana merged 35 commits intomainfrom
ENG-3044-nav-search
Mar 25, 2026
Merged

ENG-3044: Add search to Admin UI sidebar navigation#7723
galvana merged 35 commits intomainfrom
ENG-3044-nav-search

Conversation

@galvana
Copy link
Contributor

@galvana galvana commented Mar 20, 2026

Ticket ENG-3044

Description Of Changes

nav-search-demo.mp4

Add a search component to the Admin UI sidebar navigation that lets users quickly find and navigate to any page, tab, system, integration, or taxonomy type. In expanded mode, it renders as an inline autocomplete input in the sidebar. In collapsed mode, it opens a centered Spotlight-style modal. Supports Cmd+K / Ctrl+K keyboard shortcut, full arrow-key navigation, and screen-reader accessibility (ARIA combobox pattern with live region announcements).

Searchable items include:

  • All navigation pages (filtered by user permissions, Plus status, and feature flags)
  • Tabs within pages (e.g., Notifications > Messaging templates, Privacy Requests > Manual tasks)
  • System names (navigates to system detail)
  • Integration names (navigates to integration detail)
  • Taxonomy type names (navigates to taxonomy page)
  • Non-linkable page sections (e.g., Redaction patterns, Duplicate detection)

Sub-items only appear when the search query matches them, keeping the default dropdown compact. Tab definitions are co-located with their source components and imported into nav-config to prevent drift.

Performance: Systems and integrations use server-side search with a 200ms debounce, firing only after 2+ characters are typed. This scales to thousands of records without prefetching everything into memory. Static items (pages, tabs, taxonomy types) remain client-side filtered for instant results.

Code Changes

  • clients/admin-ui/src/features/common/nav/NavSearch.tsx - New search component with expanded (autocomplete) and collapsed (modal) modes
  • clients/admin-ui/src/features/common/nav/NavSearch.module.scss - Styles for both modes, dark sidebar theme, Spotlight modal
  • clients/admin-ui/src/features/common/nav/useNavSearchItems.ts - Hook that combines static nav items with server-side search for systems and integrations
  • clients/admin-ui/src/features/common/nav/nav-config.tsx - Added NavConfigTab interface and tabs field to route config
  • clients/admin-ui/src/features/common/nav/MainSideNav.tsx - Integrated NavSearch between logo and menu
  • clients/admin-ui/src/features/access-control/AccessControlTabs.tsx - Exported tab definitions for search
  • clients/admin-ui/src/features/common/NotificationTabs.tsx - Exported tab definitions for search; renamed Templates to Messaging templates
  • clients/admin-ui/src/features/privacy-requests/hooks/usePrivacyRequestTabs.ts - Exported tab definitions for search
  • clients/admin-ui/__tests__/features/common/nav/NavSearch.test.tsx - 40 tests covering both modes, filtering, keyboard shortcuts, navigation, tabs, duplicate paths, dynamic items, and accessibility

Steps to Confirm

  1. Open the Admin UI with the sidebar expanded
  2. Verify the search input appears between the logo and the nav menu
  3. Type a page name (e.g., "System") and verify matching results appear in a dropdown
  4. Click a result or press Enter to navigate
  5. Search for a tab (e.g., "Messaging") and verify it shows with the parent page name below
  6. Search for a system name and verify it navigates to the system detail page
  7. Search for an integration name and verify it navigates to the integration detail page
  8. Search for a taxonomy type (e.g., "Data categories") and verify navigation
  9. Collapse the sidebar and click the search icon
  10. Verify a centered modal opens with a search input
  11. Type a query and verify results appear inline below the input
  12. Use Arrow keys + Enter to navigate via keyboard
  13. Press Cmd+K (Mac) or Ctrl+K (Windows) from any page to open search

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:
    • No documentation updates required

Adrian Galvan and others added 2 commits March 20, 2026 16:47
Add a search component to the sidebar nav that lets users quickly find
and navigate to any page, tab, system, integration, or taxonomy type.
Supports Cmd+K shortcut, Spotlight-style modal when collapsed, and
full keyboard/screen-reader accessibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Mar 25, 2026 9:12pm
fides-privacy-center Ignored Ignored Mar 25, 2026 9:12pm

Request Review

Adrian Galvan and others added 4 commits March 20, 2026 16:49
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adrian Galvan and others added 2 commits March 20, 2026 17:27
…ess collision

The expanded nav search placeholder "Search" matched existing Cypress
selectors (input[placeholder='Search']). Changed to "Go to..." to
avoid the collision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adrian Galvan and others added 2 commits March 20, 2026 17:36
Replace eager prefetch of all systems/integrations with server-side
search that fires after 2+ characters with a 300ms debounce. This
scales to thousands of systems and integrations without loading them
all into memory on page load.

Static items (pages, tabs, taxonomy types) remain client-side filtered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@galvana galvana marked this pull request as ready for review March 21, 2026 00:41
@galvana galvana requested a review from a team as a code owner March 21, 2026 00:41
@galvana galvana requested review from speaker-ender and removed request for a team March 21, 2026 00:41
Override Ant Design CSS variables for modal content/body padding
to ensure zero padding regardless of theme configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 21, 2026

Greptile Summary

This PR adds a polished sidebar navigation search component to the Admin UI, supporting both an inline expanded mode and a Spotlight-style modal for the collapsed sidebar, with keyboard shortcuts, ARIA accessibility, server-side search for systems/integrations, and 40 tests. The implementation is well-structured, but there are a few issues to address before merging.

Key findings:

  • Permission leakage in tab search resultsNOTIFICATION_TAB_ITEMS and PRIVACY_REQUEST_TAB_ITEMS are exported as flat, unfiltered lists. The tab components themselves correctly filter by Plus status, feature flags, and scopes, but the nav search bypasses all of that. A non-Plus user with a Notifications scope will see "Messaging templates", "Digests", and "Chat providers" tabs in search results despite having no access to them. NavConfigTab should carry the same permission metadata as NavConfigRoute so configureNavRoute can filter tabs as it filters routes.
  • Custom useSearch hook not used — The project has a standard useSearch hook with disableUrlState: true support specifically for in-memory search state. The component reinvents this with raw useState + a manual debounce useEffect.
  • div elements where semantic HTML could be used — The results listbox container (line 285), group wrapper (line 291), and group label (line 298) use div where ul, section/div[role="group"], and p/span respectively would be more appropriate.
  • DEBOUNCE_MS set to 200 ms, not 300 ms as stated in the PR description.
  • Test file is 788 lines, exceeding the 700-line guideline; collapsed-mode tests should be extracted to a separate file.
  • PR exceeds 500 line changes — the combined new code (NavSearch.tsx ~369 lines, NavSearch.module.scss ~220 lines, useNavSearchItems.ts ~173 lines, NavSearch.test.tsx 788 lines) is well over the 500-line threshold.

Confidence Score: 3/5

  • Mergeable after addressing the permission leakage in tab search results and the PR size policy violation.
  • The core feature is well-implemented with good test coverage and ARIA support, but the tab permission leakage is a real UX/security concern (users see and can attempt navigation to tabs they cannot access), and the PR also exceeds the project's 500-line change policy. The remaining issues are style-level suggestions.
  • useNavSearchItems.ts, NotificationTabs.tsx, and usePrivacyRequestTabs.ts for the tab permission filtering issue; NavSearch.tsx for useSearch hook and semantic HTML.

Important Files Changed

Filename Overview
clients/admin-ui/src/features/common/nav/NavSearch.tsx New search component (369 lines) — core logic is sound, but uses custom useState/debounce instead of the project's useSearch hook, uses div where semantic HTML could be used, and the DEBOUNCE_MS constant disagrees with the PR description.
clients/admin-ui/src/features/common/nav/useNavSearchItems.ts New hook combining static nav items with server-side search — logic and data flow are clean, but tab items are included without respecting per-user permission/Plus/flag filtering that the corresponding tab components enforce.
clients/admin-ui/src/features/common/nav/nav-config.tsx Added NavConfigTab interface and tabs field to NavConfigRoute/NavGroupChild; wired up tab definitions from AccessControl, Notifications, PrivacyRequests, and Settings. Changes are minimal and well-scoped.
clients/admin-ui/src/features/common/NotificationTabs.tsx Exports NOTIFICATION_TAB_ITEMS with all 4 tabs (including Plus-only and feature-flag-gated ones) unconditionally; the component itself filters correctly at render time, but the exported constant used by nav search bypasses that filtering.
clients/admin-ui/src/features/privacy-requests/hooks/usePrivacyRequestTabs.ts Exports PRIVACY_REQUEST_TAB_ITEMS including the "Manual tasks" tab, which requires MANUAL_FIELD_READ_OWN/MANUAL_FIELD_READ_ALL scope; the tab will appear in nav search for users who lack those scopes.
clients/admin-ui/tests/features/common/nav/NavSearch.test.tsx Comprehensive 788-line test suite covering both modes, keyboard nav, ARIA, and dynamic items — exceeds the 700-line guideline and should be split into a separate file for collapsed-mode tests.

Last reviewed commit: "Fix modal padding in..."

Ant Design v5 wraps content in .ant-modal-container (not
.ant-modal-content) which adds 20px 24px padding. Target both
selectors for compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use ul/li/span instead of div for modal results list (semantic HTML)
- Split collapsed mode tests into NavSearch.collapsed.test.tsx
- Add list-style reset for ul/li elements in results

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adrian Galvan and others added 2 commits March 20, 2026 21:03
The backend LIKE search on 'ab' matches many systems containing those
letters anywhere in the name. With size=10 relevant results like
'AB InBev' were pushed off the first page. Size=50 gives better
coverage while the client-side filter still narrows to exact matches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
galvana and others added 4 commits March 23, 2026 12:17
Move collapse/expand toggle from logo area to bottom bar footer.
Logo is now a static display. In expanded mode the toggle is
right-aligned; in collapsed mode it stacks below Help and Account.

Restyle nav search input to blend into sidebar: minos fill with
subtle neutral-700 border, neutral-800 on hover/focus, Cmd+K hint
only visible on focus.

Standardize bottom bar button styles across Help, Account, and
collapse toggle with shared navBottomButton class.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep the logo clickable for users accustomed to that behavior. The
footer button remains as a secondary affordance for discoverability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment on lines +10 to +15
/** Shared tab definitions used by both the tab bar and nav search. */
export const ACCESS_CONTROL_TAB_ITEMS: NavConfigTab[] = [
{ title: "Summary", path: ACCESS_CONTROL_SUMMARY_ROUTE },
{ title: "Request log", path: ACCESS_CONTROL_REQUEST_LOG_ROUTE },
];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted the tab definitions to a const so we prevent drift as we add new tabs. The nav search uses these consts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where you're going.
Not sure if in scope, but I think our method of keeping track of paths is flawed.
If we stored paths as a record or enum, we could enforce uniqueness and also define the path of NavConfigTab as being a strict value from that record/enum so that we can make sure that urls aren't missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, a centralized path record/enum would be a nice improvement for enforcing uniqueness. I'll note it as a follow-up since it touches more than just the search feature.

Copy link
Contributor

@speaker-ender speaker-ender left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good and very cool!
Some small nits, comments, and questions.

Comment on lines +10 to +15
/** Shared tab definitions used by both the tab bar and nav search. */
export const ACCESS_CONTROL_TAB_ITEMS: NavConfigTab[] = [
{ title: "Summary", path: ACCESS_CONTROL_SUMMARY_ROUTE },
{ title: "Request log", path: ACCESS_CONTROL_REQUEST_LOG_ROUTE },
];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like where you're going.
Not sure if in scope, but I think our method of keeping track of paths is flawed.
If we stored paths as a record or enum, we could enforce uniqueness and also define the path of NavConfigTab as being a strict value from that record/enum so that we can make sure that urls aren't missed.

},
];
const menuItems = ACCESS_CONTROL_TAB_ITEMS.map((tab) => ({
key: tab.path === ACCESS_CONTROL_SUMMARY_ROUTE ? "summary" : "request-log",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the key can just be the path if it is unique

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched to using tab.path as the key and simplified the component.

Comment on lines -166 to +167
className={`inline-flex cursor-pointer p-0 ${styles.collapseToggle}`}
className={styles.logoToggle}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we do prefer to use tailwind classes over css module styles when possible.
I think this is a pattern in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted the layout wrappers and the collapsed button to Tailwind classes. The remaining SCSS covers Ant component overrides that require :global() selectors (modal, input, dropdown styling), which can't be expressed in Tailwind.

Comment on lines +98 to +113
const handleSelect = useCallback(
(key: string) => {
const path = keyToPath.get(key) ?? key;
justSelectedRef.current = true;
router.push(path);
setOpen(false);
setSearchValue("");
inputRef.current?.blur();
modalInputRef.current?.blur();
// Reset the guard after the event loop settles
setTimeout(() => {
justSelectedRef.current = false;
}, 0);
},
[router, keyToPath],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if fully possible or makes sense for certain results but it would be nice if the routing was handled by a link component rather than a callback for simplicity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched the modal result items to NextLink in the new NavSearchModal.tsx. The expanded mode still uses a callback since Ant's AutoComplete controls rendering via onSelect.

width={480}
style={MODAL_POSITION}
className={styles.searchModal}
destroyOnClose
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the inner content handled some of the estate, I think the destroyOnClose would handle some of state resetting without having to do it manually + using a callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroyOnClose handles the modal's internal DOM cleanup, but searchValue state lives in the parent component (needed to drive the input and filtered results), so we still need to clear it explicitly on close. With the component split, this is cleaner since handleClose in NavSearchModal just calls setOpen(false) and setSearchValue("").

- Split NavSearch into NavSearchExpanded + NavSearchModal components
- Extract NavSearchResultItem as separate component
- Use NextLink for modal result items instead of router.push callback
- Switch to react-hotkeys-hook for Cmd+K/Ctrl+K shortcut
- Rewrite grouping/item construction with reduce/flatMap (no mutations)
- Use pluralize utility for result count announcement
- Use path as key in AccessControlTabs
- Convert layout wrappers to Tailwind, keep Ant overrides in SCSS
- Fix aria-controls to always reference the listbox ID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@speaker-ender speaker-ender left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment about tests that I've seen as a re-occurring theme in PRs.
Otherwise looks good!

Comment on lines +6 to +8
// Mock fidesui to avoid jsdom incompatibilities with Ant Design components.
// NavSearch imports: AutoComplete, Icons, Input, InputRef, Modal
jest.mock("fidesui", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just calling this out as something to investigate and solve in the future as this isn't best practice to mock things like this.
It's codifying functionality that might have no bearing on reality and can cause false positives and negatives in tests.
Bad tests can be worse than no tests at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this as part of a separate ticket https://ethyca.atlassian.net/browse/ENG-3161

# Conflicts:
#	clients/admin-ui/src/features/access-control/AccessControlTabs.tsx
@galvana galvana added this pull request to the merge queue Mar 25, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Mar 25, 2026
These icons are used in MainSideNav.tsx for the nav collapse toggle
but were missing from the curated carbon.ts export list introduced
in #7745, causing typecheck failures in the merge queue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@galvana galvana enabled auto-merge March 25, 2026 21:22
@galvana galvana added this pull request to the merge queue Mar 25, 2026
Merged via the queue into main with commit a1b674f Mar 25, 2026
46 of 49 checks passed
@galvana galvana deleted the ENG-3044-nav-search branch March 25, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants